{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "43840b4a-f9dc-4076-b1bf-5276da05f4ea",
   "metadata": {
    "tags": []
   },
   "source": [
    "# Interactive visualizer\n",
    "Using [Interactive visualizers](https://kaolin.readthedocs.io/en/latest/modules/kaolin.visualize.html) you can bring your own renderer and connect it to the visualizer with live mouse camera control right in the notebook, for example to debug your custom rendering function. The main condition is that the renderer has to take a [Camera](https://kaolin.readthedocs.io/en/latest/modules/kaolin.render.camera.camera.html#kaolin-render-camera-camera) as input.\n",
    "\n",
    "In this notebook, we show how to visualize differentiable rendering of a multi-material mesh from ShapeNet using spherical gaussians lighting. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "69114969",
   "metadata": {
    "scrolled": true,
    "tags": []
   },
   "outputs": [],
   "source": [
    "import copy\n",
    "import glob\n",
    "import math\n",
    "import logging\n",
    "import numpy as np\n",
    "import os\n",
    "import sys\n",
    "import torch\n",
    "\n",
    "from tutorial_common import COMMON_DATA_DIR\n",
    "import kaolin as kal\n",
    "\n",
    "import nvdiffrast\n",
    "glctx = nvdiffrast.torch.RasterizeGLContext(False, device='cuda')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd8f9d9e",
   "metadata": {},
   "source": [
    "## Load Mesh information"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "4963d2ce",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SurfaceMesh object with batching strategy FIXED\n",
      "            vertices: [1, 5002, 3] (torch.float32)[cuda:0]  \n",
      "               faces: [10000, 3] (torch.int64)[cuda:0]  \n",
      "             normals: [1, 5002, 3] (torch.float32)[cuda:0]  \n",
      "    face_normals_idx: [1, 10000, 3] (torch.int64)[cuda:0]  \n",
      "                 uvs: [1, 5505, 2] (torch.float32)[cuda:0]  \n",
      "        face_uvs_idx: [1, 10000, 3] (torch.int64)[cuda:0]  \n",
      "material_assignments: [1, 10000] (torch.int16)[cuda:0]  \n",
      "           materials: [\n",
      "                      0: list of length 1\n",
      "                      ]\n",
      "       face_vertices: if possible, computed on access from: (faces, vertices)\n",
      "        face_normals: if possible, computed on access from: (normals, face_normals_idx) or (vertices, faces)\n",
      "            face_uvs: if possible, computed on access from: (uvs, face_uvs_idx)\n",
      "      vertex_normals: if possible, computed on access from: (faces, face_normals)\n",
      "     vertex_tangents: if possible, computed on access from: (faces, vertices, face_uvs)\n"
     ]
    }
   ],
   "source": [
    "# Set KAOLIN_TEST_SHAPENETV2_PATH env variable, or replace by your shapenet path\n",
    "SHAPENETV2_PATH = os.getenv('KAOLIN_TEST_SHAPENETV2_PATH')\n",
    "\n",
    "if SHAPENETV2_PATH is not None:\n",
    "    ds = kal.io.shapenet.ShapeNetV2(root=SHAPENETV2_PATH,\n",
    "                                    categories=['car'],\n",
    "                                    train=True, split=1.,\n",
    "                                    with_materials=True,\n",
    "                                    output_dict=True)\n",
    "    mesh = ds[0]['mesh']\n",
    "else:\n",
    "    # Load a specific obj instead\n",
    "    OBJ_PATH = os.path.join(COMMON_DATA_DIR, 'meshes', 'fox.obj')\n",
    "    mesh = kal.io.obj.import_mesh(OBJ_PATH, with_materials=True, with_normals=True, triangulate=True)\n",
    "\n",
    "# Batch, move to GPU and center and normalize vertices in the range [-0.5, 0.5]\n",
    "mesh = mesh.to_batched().cuda()\n",
    "mesh.vertices = kal.ops.pointcloud.center_points(mesh.vertices, normalize=True)\n",
    "print(mesh)\n",
    "\n",
    "diffuse_maps = [m['map_Kd'].unsqueeze(0).cuda().float() / 255. if 'map_Kd' in m else\n",
    "                m['Kd'].reshape(1, 1, 1, 3).cuda()\n",
    "                for m in mesh.materials[0]]\n",
    "specular_maps = [m['map_Ks'].unsqueeze(0).cuda().float() / 255. if 'map_Ks' in m else\n",
    "                 m['Ks'].reshape(1, 1, 1, 3).cuda()\n",
    "                 for m in mesh.materials[0]]\n",
    "\n",
    "# Use a single diffuse color as backup when map doesn't exist (and face_uvs_idx == -1)\n",
    "mesh.uvs = torch.nn.functional.pad(mesh.uvs, (0, 0, 0, 1))\n",
    "mesh.face_uvs_idx[mesh.face_uvs_idx == -1] = mesh.uvs.shape[1] - 1"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "03e898f4",
   "metadata": {},
   "source": [
    "## Instantiate a camera\n",
    "\n",
    "With the general constructor `Camera.from_args()` the underlying constructors are `CameraExtrinsics.from_lookat()` and `PinholeIntrinsics.from_fov` we will use this camera as a starting point for the visualizers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c6eee7ab",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "camera = kal.render.camera.Camera.from_args(eye=torch.tensor([2., 1., 1.], device='cuda'),\n",
    "                                            at=torch.tensor([0., 0., 0.]),\n",
    "                                            up=torch.tensor([1., 1., 1.]),\n",
    "                                            fov=math.pi * 45 / 180,\n",
    "                                            width=512, height=512, device='cuda')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4fff8eb1",
   "metadata": {},
   "source": [
    "## Rendering a mesh\n",
    "\n",
    "Here we are rendering the loaded mesh with [nvdiffrast](https://github.com/NVlabs/nvdiffrast) using the camera object created above and use both diffuse and specular reflectance for lighting.\n",
    "\n",
    "For more information on lighting in Kaolin see [diffuse](./diffuse_lighting.ipynb) and [specular](./sg_specular_lighting.ipynb) tutorials and the [documentation](https://kaolin.readthedocs.io/en/latest/modules/kaolin.render.lighting.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "5e4b8a49",
   "metadata": {
    "tags": []
   },
   "outputs": [],
   "source": [
    "# Those are the parameters used to define the Spherical gaussian\n",
    "azimuth = torch.zeros((1,), device='cuda')\n",
    "elevation = torch.full((1,), math.pi / 3., device='cuda')\n",
    "amplitude = torch.full((1, 3), 3., device='cuda')\n",
    "sharpness = torch.full((1,), 5., device='cuda')\n",
    "# We will use this variable to enable / disable specular reflectance\n",
    "global apply_specular\n",
    "apply_specular = True\n",
    "\n",
    "def generate_pinhole_rays_dir(camera, height, width, device='cuda'):\n",
    "    \"\"\"Generate centered grid.\n",
    "    \n",
    "    This is a utility function for specular reflectance with spherical gaussian.\n",
    "    \"\"\"\n",
    "    pixel_y, pixel_x = torch.meshgrid(\n",
    "        torch.arange(height, device=device),\n",
    "        torch.arange(width, device=device),\n",
    "        indexing='ij'\n",
    "    )\n",
    "    pixel_x = pixel_x + 0.5  # scale and add bias to pixel center\n",
    "    pixel_y = pixel_y + 0.5  # scale and add bias to pixel center\n",
    "\n",
    "    # Account for principal point (offsets from the center)\n",
    "    pixel_x = pixel_x - camera.x0\n",
    "    pixel_y = pixel_y + camera.y0\n",
    "\n",
    "    # pixel values are now in range [-1, 1], both tensors are of shape res_y x res_x\n",
    "    # Convert to NDC\n",
    "    pixel_x = 2 * (pixel_x / width) - 1.0\n",
    "    pixel_y = 2 * (pixel_y / height) - 1.0\n",
    "\n",
    "    ray_dir = torch.stack((pixel_x * camera.tan_half_fov(kal.render.camera.intrinsics.CameraFOV.HORIZONTAL),\n",
    "                           -pixel_y * camera.tan_half_fov(kal.render.camera.intrinsics.CameraFOV.VERTICAL),\n",
    "                           -torch.ones_like(pixel_x)), dim=-1)\n",
    "\n",
    "    ray_dir = ray_dir.reshape(-1, 3)    # Flatten grid rays to 1D array\n",
    "    ray_orig = torch.zeros_like(ray_dir)\n",
    "\n",
    "    # Transform from camera to world coordinates\n",
    "    ray_orig, ray_dir = camera.extrinsics.inv_transform_rays(ray_orig, ray_dir)\n",
    "    ray_dir /= torch.linalg.norm(ray_dir, dim=-1, keepdim=True)\n",
    "\n",
    "    return ray_dir[0].reshape(1, height, width, 3)\n",
    "\n",
    "\n",
    "def base_render(mesh, camera, height, width):\n",
    "    \"\"\"Base function for rendering using separate height and width, assuming batch_size=1\"\"\"\n",
    "    vertices_camera = camera.extrinsics.transform(mesh.vertices)\n",
    "    face_vertices_camera = kal.ops.mesh.index_vertices_by_faces(\n",
    "        vertices_camera, mesh.faces)\n",
    "    face_normals_z = kal.ops.mesh.face_normals(\n",
    "        face_vertices_camera,\n",
    "        unit=True\n",
    "    )[..., -1:].contiguous()\n",
    "\n",
    "    # Projection: nvdiffrast take clip coordinates as input to apply barycentric perspective correction.\n",
    "    # Using `camera.intrinsics.transform(vertices_camera) would return the normalized device coordinates.\n",
    "    proj = camera.projection_matrix()[None]\n",
    "    homogeneous_vecs = kal.render.camera.up_to_homogeneous(\n",
    "        vertices_camera\n",
    "    )[..., None]\n",
    "    vertices_clip = (proj @ homogeneous_vecs).squeeze(-1)\n",
    "\n",
    "    rast = nvdiffrast.torch.rasterize(\n",
    "        glctx, vertices_clip, mesh.faces.int(),\n",
    "        (height, width), grad_db=False\n",
    "    )\n",
    "    # nvdiffrast rasteriztion output is y-up, we need to flip as our display is y-down\n",
    "    rast0 = torch.flip(rast[0], dims=(1,))\n",
    "    hard_mask = rast0[:, :, :, -1:] != 0\n",
    "    face_idx = (rast0[..., -1].long() - 1).contiguous()\n",
    "\n",
    "    uv_map = nvdiffrast.torch.interpolate(\n",
    "        mesh.uvs, rast0, mesh.face_uvs_idx[0, ...].int()\n",
    "    )[0] % 1.\n",
    "    \n",
    "    if mesh.has_attribute('normals') and mesh.has_attribute('face_normals_idx'):\n",
    "        im_world_normals = nvdiffrast.torch.interpolate(\n",
    "            mesh.normals, rast0, mesh.face_normals_idx[0, ...].int())[0]\n",
    "    else:\n",
    "        im_world_normals = nvdiffrast.torch.interpolate(\n",
    "            mesh.face_normals.reshape(len(mesh), -1, 3), rast0,\n",
    "            torch.arange(mesh.faces.shape[0] * 3, device='cuda', dtype=torch.int).reshape(-1, 3)\n",
    "        )[0]\n",
    "    \n",
    "    batch_idx = torch.arange(len(mesh), device='cuda', dtype=torch.long).reshape(\n",
    "        len(mesh), 1, 1).expand(len(mesh), height, width)\n",
    "    \n",
    "    im_cam_normals = face_normals_z[batch_idx, face_idx] * (face_idx.unsqueeze(-1) != -1)\n",
    "    im_world_normals = im_world_normals * torch.sign(im_cam_normals)\n",
    "    albedo = torch.zeros(\n",
    "        (1, height, width, 3),\n",
    "        dtype=torch.float, device='cuda'\n",
    "    )\n",
    "    spec_albedo = torch.zeros(\n",
    "        (1, height, width, 3),\n",
    "        dtype=torch.float, device='cuda'\n",
    "    )\n",
    "    # Obj meshes can be composed of multiple materials\n",
    "    # so at rendering we need to interpolate from corresponding materials\n",
    "    im_material_idx = mesh.material_assignments[0, ...][face_idx]\n",
    "    im_material_idx[face_idx == -1] = -1\n",
    "\n",
    "    for i, material in enumerate(diffuse_maps):\n",
    "        mask = im_material_idx == i\n",
    "        mask_idx = torch.nonzero(mask, as_tuple=False)\n",
    "        _texcoords = uv_map[mask]\n",
    "        _texcoords[:, 1] = -_texcoords[:, 1]\n",
    "        if _texcoords.shape[0] > 0:\n",
    "            pixel_val = nvdiffrast.torch.texture(\n",
    "                diffuse_maps[i].contiguous(),\n",
    "                _texcoords.reshape(1, 1, -1, 2).contiguous(),\n",
    "                filter_mode='linear'\n",
    "            )\n",
    "            albedo[mask] = pixel_val[0, 0]\n",
    "            pixel_val = nvdiffrast.torch.texture(\n",
    "                specular_maps[i].contiguous(),\n",
    "                _texcoords.reshape(1, 1, -1, 2).contiguous(),\n",
    "                filter_mode='linear'\n",
    "            )\n",
    "            spec_albedo[mask] = pixel_val[0, 0] #.permute(1, 0)\n",
    "    img = torch.zeros((1, height, width, 3),\n",
    "                      dtype=torch.float, device='cuda')\n",
    "    sg_x, sg_y, sg_z = kal.ops.coords.spherical2cartesian(azimuth, elevation)\n",
    "    directions = torch.stack(\n",
    "        [sg_x, sg_z, sg_y],\n",
    "        dim=-1\n",
    "    )\n",
    "    im_world_normals = im_world_normals[hard_mask.squeeze(-1)]\n",
    "    diffuse_effect = kal.render.lighting.sg_diffuse_inner_product(\n",
    "        amplitude, directions, sharpness,\n",
    "        im_world_normals,\n",
    "        albedo[hard_mask.squeeze(-1)]\n",
    "    )\n",
    "    img[hard_mask.squeeze(-1)] = diffuse_effect\n",
    "    global apply_specular\n",
    "    if apply_specular:\n",
    "        rays_d = generate_pinhole_rays_dir(camera, height, width)\n",
    "        specular_effect = kal.render.lighting.sg_warp_specular_term(\n",
    "            amplitude, directions, sharpness,\n",
    "            im_world_normals,\n",
    "            torch.full((im_world_normals.shape[0],), 0.5, device='cuda'),\n",
    "            -rays_d[hard_mask.squeeze(-1)],\n",
    "            spec_albedo[hard_mask.squeeze(-1)]\n",
    "        )\n",
    "        img[hard_mask.squeeze(-1)] += specular_effect\n",
    "\n",
    "    # 'img' is the displayed image, while the other value `face_idx` is only printed on query\n",
    "    return {\n",
    "        'img': (torch.clamp(img * hard_mask, 0., 1.)[0] * 255.).to(torch.uint8),\n",
    "        'face_idx': face_idx[0]\n",
    "    }\n",
    "\n",
    "def render(camera):\n",
    "    \"\"\"Render using camera dimension.\n",
    "    \n",
    "    This is the main function provided to the interactive visualizer\n",
    "    \"\"\"\n",
    "    return base_render(mesh, camera, camera.height, camera.width)\n",
    "\n",
    "def lowres_render(camera):\n",
    "    \"\"\"Render with lower dimension.\n",
    "    \n",
    "    This function will be used as a \"fast\" rendering used when the mouse is moving to avoid slow down.\n",
    "    \"\"\"\n",
    "    return base_render(mesh, camera, int(camera.height / 4), int(camera.width / 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "383f1ffa-936c-416f-8779-d20fe28e7230",
   "metadata": {},
   "source": [
    "## Turntable visualizer\n",
    "This is a simple visualizer useful to inspect a small object.\n",
    "\n",
    "You can move around with the mouse (left button) and zoom with the mouse wheel.\n",
    "See the [documentation](https://kaolin.readthedocs.io/en/latest/modules/kaolin.visualize.html#kaolin.visualize.IpyTurntableVisualizer) to customize the sensitivity."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "f10f6ab7-662c-4cf6-8af4-5f28ae79d795",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "883f99f3b60141258fcc4dcf0db9d314",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Canvas(height=512, width=512)"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "2ad9e66e6172456db0d425ebede7dbbf",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "visualizer = kal.visualize.IpyTurntableVisualizer(\n",
    "    512, 512, copy.deepcopy(camera), render,\n",
    "    fast_render=lowres_render, max_fps=24, world_up_axis=1)\n",
    "visualizer.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8ff3945-66a4-47af-a3c0-4428c2adf6cf",
   "metadata": {},
   "source": [
    "## First person visualizer\n",
    "This is a visualizer useful to inspect details on an object, or a big scene.\n",
    "\n",
    "You can move the orientation of the camera with the mouse left button, move the camera around with the mouse right button or\n",
    "the keys 'i' (up), 'k' (down), 'j' (left), 'l' (right), 'o' (forward), 'u' (backward)\n",
    "\n",
    "See the [documentation](https://kaolin.readthedocs.io/en/latest/modules/kaolin.visualize.html#kaolin.visualize.IpyFirstPersonVisualizer) to customize the sensitivity and keys.\n",
    "\n",
    "--------------------\n",
    "*Note: camera are mutable in the visualizer. If you want to keep track of the camera position you can remove the `copy.deepcopy` on camera argument or you can check `visualizer.camera`*"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "27a16dea",
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "74c34ff2505c42d4bf952ea9d14715fb",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Canvas(height=512, width=512)"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "d0cc2f35f68b405a96512c659d67568f",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "visualizer = kal.visualize.IpyFirstPersonVisualizer(\n",
    "    512, 512, copy.deepcopy(camera), render, fast_render=lowres_render,\n",
    "    max_fps=24, world_up=torch.tensor([0., 1., 0.], device='cuda'))\n",
    "visualizer.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0c6a9142",
   "metadata": {},
   "source": [
    "# Adding events and other widgets\n",
    "\n",
    "The visualizer is modular.\n",
    "Here we will add:\n",
    "* sliders to control the spherical gaussian parameters (see [ipywidgets tutorial](https://ipywidgets.readthedocs.io/en/stable/examples/Using%20Interact.html) for more info).\n",
    "* A key event to 'space' to enable / disable specular reflectance (see [ipyevents documentation](https://github.com/mwcraig/ipyevents/blob/main/docs/events.ipynb)) to see all the events that can be caught.\n",
    "\n",
    "In general if you want to modify the rendering function you can use global variables or make a class (with the rendering function being a method)\n",
    "\n",
    "-------------\n",
    "More info on spherical gaussians parameters in our [sg_specular_lighting.ipynb](./sg_specular_lighting.ipynb) tutorial\n",
    "and [documentation](https://kaolin.readthedocs.io/en/latest/modules/kaolin.render.lighting.html)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "0a9fd84e",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "a1418b4982154f6381b6669713503672",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "HBox(children=(Canvas(height=512, width=512), interactive(children=(FloatSlider(value=1.0471975803375244, desc…"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "data": {
      "application/vnd.jupyter.widget-view+json": {
       "model_id": "07ba006798f4479183455879fc81d681",
       "version_major": 2,
       "version_minor": 0
      },
      "text/plain": [
       "Output()"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from ipywidgets import interactive, HBox, FloatSlider\n",
    "\n",
    "def additional_event_handler(visualizer, event):\n",
    "    \"\"\"Event handler to be provided to Kaolin's visualizer\"\"\"\n",
    "    with visualizer.out: # This is for catching print and errors\n",
    "        if event['type'] == 'keydown' and event['key'] == ' ':\n",
    "            global apply_specular\n",
    "            apply_specular = not apply_specular\n",
    "            visualizer.render_update()\n",
    "            return False\n",
    "        return True\n",
    "\n",
    "visualizer = kal.visualize.IpyTurntableVisualizer(\n",
    "    512, 512, copy.deepcopy(camera), render,\n",
    "    fast_render=lowres_render, max_fps=24,\n",
    "    additional_event_handler=additional_event_handler,\n",
    "    additional_watched_events=['keydown'] # We need to now watch for key press event\n",
    ")\n",
    "# we don't call visualizer.show() here\n",
    "\n",
    "def sliders_callback(new_elevation, new_azimuth, new_amplitude, new_sharpness):\n",
    "    \"\"\"ipywidgets sliders callback\"\"\"\n",
    "    with visualizer.out: # This is in case of bug\n",
    "        elevation[:] = new_elevation\n",
    "        azimuth[:] = new_azimuth\n",
    "        amplitude[:] = new_amplitude\n",
    "        sharpness[:] = new_sharpness\n",
    "        # this is how we request a new update\n",
    "        visualizer.render_update()\n",
    "        \n",
    "elevation_slider = FloatSlider(\n",
    "    value=elevation.item(),\n",
    "    min=-math.pi / 2.,\n",
    "    max=math.pi / 2.,\n",
    "    step=0.1,\n",
    "    description='Elevation:',\n",
    "    continuous_update=True,\n",
    "    readout=True,\n",
    "    readout_format='.1f',\n",
    ")\n",
    "\n",
    "azimuth_slider = FloatSlider(\n",
    "    value=azimuth.item(),\n",
    "    min=-math.pi,\n",
    "    max=math.pi,\n",
    "    step=0.1,\n",
    "    description='Azimuth:',\n",
    "    continuous_update=True,\n",
    "    readout=True,\n",
    "    readout_format='.1f',\n",
    ")\n",
    "\n",
    "amplitude_slider = FloatSlider(\n",
    "    value=amplitude[0,0].item(),\n",
    "    min=0.1,\n",
    "    max=20.,\n",
    "    step=0.1,\n",
    "    description='Amplitude:\\n',\n",
    "    continuous_update=True,\n",
    "    readout=True,\n",
    "    readout_format='.1f',\n",
    ")\n",
    "\n",
    "sharpness_slider = FloatSlider(\n",
    "    value=sharpness.item(),\n",
    "    min=0.1,\n",
    "    max=20.,\n",
    "    step=0.1,\n",
    "    description='Sharpness:\\n',\n",
    "    continuous_update=True,\n",
    "    readout=True,\n",
    "    readout_format='.1f',\n",
    ")\n",
    "\n",
    "interactive_slider = interactive(\n",
    "    sliders_callback,\n",
    "    new_elevation=elevation_slider,\n",
    "    new_azimuth=azimuth_slider,\n",
    "    new_amplitude=amplitude_slider,\n",
    "    new_sharpness=sharpness_slider\n",
    ")\n",
    "\n",
    "# We combine all the widgets and the visualizer canvas and output in a single display\n",
    "full_output = HBox([visualizer.canvas, interactive_slider])\n",
    "display(full_output, visualizer.out)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
